package play.modules.facebook;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import org.apache.commons.codec.binary.Base64;
import play.Logger;
import play.exceptions.UnexpectedException;
import play.libs.WS;
import play.mvc.Http;

import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.concurrent.Future;

/**
 * Java version of https://github.com/facebook/php-sdk/ by Naitik Shah <naitik@facebook.com>
 * Translated by: Dallan Quass <dallan@gmail.com>
 */
public class Facebook {
   public static final String VERSION = "2.1.2";

  /**
    * Maps aliases to Facebook domains.
    */
   public static final Map<String,String> DOMAIN_MAP = Utils.createMap(
          "api", "https://api.facebook.com/",
          "api_read", "https://api-read.facebook.com/",
          "graph", "https://graph.facebook.com/",
          "www", "https://www.facebook.com/"
  );

  /**
    * The Application ID.
    */
   protected String appId;

  /**
    * The Application API Secret.
    */
   protected String apiSecret;

  /**
    * The active user session, if one is available.
    */
   protected Session session;

  /**
    * The data from the signed_request token.
    */
   protected SignedRequest signedRequest;

  /**
    * Indicates that we already loaded the session as best as we could.
    */
   protected boolean sessionLoaded = false;

  /**
    * Indicates if Cookie support should be enabled.
    */
   protected boolean cookieSupport;

   /**
    * Base domain for the Cookie.
    */
   protected String baseDomain;

   /**
    * Indicates if the CURL based @ syntax for file uploads is enabled.
    * UNSUPPORTED
    */
   protected boolean fileUploadSupport;

  /**
    * Initialize a Facebook Application.
    *
    * The configuration:
    * @param appId: the application ID
    * @param apiSecret: the application secret
    * @param cookieSupport true to enable cookie support
    * @param baseDomain domain for the cookie
    * @param fileUploadSupport true if file uploads are enabled
   */
   public Facebook(String appId, String apiSecret, boolean cookieSupport, String baseDomain, boolean fileUploadSupport) {
      this.appId = appId;
      this.apiSecret = apiSecret;
      setCookieSupport(cookieSupport);
      setBaseDomain(baseDomain);
      setFileUploadSupport(fileUploadSupport);
   }

   public Facebook(String appId, String apiSecret) {
      this(appId, apiSecret, false, "", false);
   }

   public Facebook(String appId, String apiSecret, boolean cookieSupport) {
      this(appId, apiSecret, cookieSupport, "", false);
   }

   public Facebook setAppId(String appId) {
      this.appId = appId;
      return this;
   }

   public String getAppId() {
      return appId;
   }

   public Facebook setApiSecret(String apiSecret) {
      this.apiSecret = apiSecret;
      return this;
   }

   public String getApiSecret() {
      return apiSecret;
   }

   public Facebook setCookieSupport(boolean cookieSupport) {
      this.cookieSupport = cookieSupport;
      return this;
   }

   public boolean useCookieSupport() {
      return cookieSupport;
   }

   public Facebook setBaseDomain(String baseDomain) {
      this.baseDomain = baseDomain;
      return this;
   }

   public String getBaseDomain() {
      return baseDomain;
   }

   public Facebook setFileUploadSupport(boolean fileUploadSupport) {
      this.fileUploadSupport = fileUploadSupport;
      return this;
   }

   public boolean useFileUploadSupport() {
      return fileUploadSupport;
   }

  /**
    * Get the data from a signed_request token
   * @return SignedRequest
    */
   public SignedRequest getSignedRequest() {
      if (signedRequest == null) {
         String signedRequestString = Http.Request.current().params.get("signed_request");
         if (signedRequestString != null) {
            signedRequest = parseSignedRequest(signedRequestString);
         }
      }
      return signedRequest;
   }

  /**
    * Set the Session.
    *
    * @param session the session
    * @param writeCookie indicate if a cookie should be written. this value is ignored if cookie support has been disabled.
   * @return this
    */
   public Facebook setSession(Session session, boolean writeCookie) {
      this.session = validateSessionObject(session);
      sessionLoaded = true;
      if (writeCookie) {
         setCookieFromSession(this.session);
      }
      return this;
   }

   public Facebook setSession(Session session) {
      return setSession(session, true);
   }

  /**
    * Get the session object. This will automatically look for a signed session
    * sent via the signed_request, Cookie or Query Parameters if needed.
    *
    * @return the session
    */
   public Session getSession() {
      if (!sessionLoaded) {
         Session sess = null;
         boolean writeCookie = true;

         SignedRequest signedRequest = getSignedRequest();
         if (signedRequest != null) {
            // sig is good, use the signedRequest
            Logger.trace("create session from signed request");
            sess = createSessionFromSignedRequest(signedRequest);
         }

         // try loading session from $_REQUEST
         String sessionString = Http.Request.current().params.get("session");
         if (sess == null && sessionString != null) {
            Logger.trace("create session from request");
            sess = new Session(new JsonParser().parse(sessionString));
            sess = validateSessionObject(sess);
         }

         // try loading session from cookie if necessary
         if (sess == null && useCookieSupport()) {
            String cookieName = getSessionCookieName();
            Http.Cookie cookie = Http.Request.current().cookies.get(cookieName);
            if (cookie != null) {
               Logger.trace("create session from cookie: "+cookieName);
               sess = new Session(cookie.value);
               sess = validateSessionObject(sess);
               // write only if we need to delete a invalid session cookie
               writeCookie = (sess == null);
            }
         }

         setSession(sess, writeCookie);
      }

      return session;
   }

  /**
    * Get the UID from the session.
    *
    * @return the UID if available
    */
   public String getUser() {
      Session sess = getSession();
      return (sess != null ? sess.getUid() : null);
   }

  /**
    * Gets a OAuth access token.
    *
    * @return String the access token
    */
   public String getAccessToken() {
      Session sess = getSession();
      if (sess != null) {
         return sess.getAccessToken();
      }
      else {
         return getAppId() + "|" + getApiSecret();
      }
   }

  /**
    * Get a Login URL for use with redirects. By default, full page redirect is
    * assumed. If you are using the generated URL with a window.open() call in
    * JavaScript, you can pass in display=popup as part of the $params.
    *
    * The parameters:
    * - next: the url to go to after a successful login
    * - cancel_url: the url to go to after the user cancels
    * - req_perms: comma separated list of requested extended perms
    * - display: can be "page" (default, full page) or "popup"
    *
    * @param params provide custom parameters
    * @return the URL for the login flow
    */
   public String getLoginUrl(Map<String,String> params) {
      String currentUrl = getCurrentUrl();
      return getUrl(
         "www",
         "login.php",
         Utils.array_merge(Utils.createMap(
                 "api_key", getAppId(),
                 "cancel_url", currentUrl,
                 "display", "page",
                 "fbconnect", "1",
                 "next", currentUrl,
                 "return_session", "1",
                 "session_version", "3",
                 "v", "1.0"),
                 params));
   }
   public String getLoginUrl() {
      return getLoginUrl(null);
   }

  /**
    * Get a Logout URL suitable for use with redirects.
    *
    * The parameters:
    * - next: the url to go to after a successful logout
    *
    * @param params provide custom parameters
    * @return the URL for the logout flow
    */
   public String getLogoutUrl(Map<String,String> params) {
      return getUrl(
         "www",
         "logout.php",
         Utils.array_merge(Utils.createMap(
                 "next", getCurrentUrl(),
                 "access_token", getAccessToken()),
                 params));
   }
   public String getLogoutUrl() {
      return getLogoutUrl(null);
   }

  /**
    * Get a login status URL to fetch the status from facebook.
    *
    * The parameters:
    * - ok_session: the URL to go to if a session is found
    * - no_session: the URL to go to if the user is not connected
    * - no_user: the URL to go to if the user is not signed into facebook
    *
    * @param params provide custom parameters
    * @return the URL for the logout flow
    */
   public String getLoginStatusUrl(Map<String,String> params) {
      String currentUrl = getCurrentUrl();
      return getUrl(
         "www",
         "extern/login_status.php",
         Utils.array_merge(Utils.createMap(
                 "api_key", getAppId(),
                 "no_session", currentUrl,
                 "no_user", currentUrl,
                 "ok_session", currentUrl,
                 "session_version", "3"),
                 params));
   }
   public String getLoginStatusUrl() {
      return getLoginStatusUrl(null);
   }

   /**
    * Make an API call.
    *
    * @param path the api path
    * @param params the API call parameters
    * @return the Json response object
    */
   public JsonElement api(String path, Map<String,String> params) throws FacebookApiException {
     return api(path, "GET", params);
   }
   public JsonElement api(String path) throws FacebookApiException {
     return api(path, "GET", new HashMap<String,String>());
   }
   public Future<WS.HttpResponse> apiAsync(String path, Map<String,String> params) throws FacebookApiException {
      return apiAsync(path, "GET", params);
   }
   public Future<WS.HttpResponse> apiAsync(String path) throws FacebookApiException {
      return apiAsync(path, "GET", new HashMap<String,String>());
   }

   private void prepareApiParams(Map<String, String> params) {
      params.put("api_key", getAppId());
      params.put("format", "json-strings");
   }

   /**
    * Invoke the old restserver.php endpoint.
    *
    * @param params method call object
    * @return the Json response object
    * @throws FacebookApiException if there is a problem accessing facebook
    */
   public JsonElement api(Map<String,String> params) throws FacebookApiException {
      // _restserver version
      prepareApiParams(params);
      return oauthRequest(getApiUrl(params.get("method")), params);
   }
   public Future<WS.HttpResponse> apiAsync(Map<String,String> params) {
      prepareApiParams(params);
      return oauthAsyncRequest(getApiUrl(params.get("method")), params);
   }

   /**
    * Invoke the Graph API.
    *
    * @param path the path (required)
    * @param method the http method (default 'GET')
    * @param params the query/post data
    * @return the STILL-ENCODED response object
    * @throws FacebookApiException if there is a problem accessing facebook
    */
   public JsonElement api(String path, String method, Map<String,String> params) throws FacebookApiException {
      // _graph version
      params.put("method", method); // method override as we always do a POST
      return oauthRequest(getUrl("graph", path), params);
   }
   public Future<WS.HttpResponse> apiAsync(String path, String method, Map<String,String> params) throws FacebookApiException {
      // _graph version
      params.put("method", method); // method override as we always do a POST
      return oauthAsyncRequest(getUrl("graph", path), params);
   }

  /**
    *  Convenience method
    * @param query to send to fql.query method
    * @return query result
    */
   public JsonElement query(String query) throws FacebookApiException {
      return api(Utils.createMap(
              "method", "fql.query",
              "query", query));
   }
   public Future<WS.HttpResponse> queryAsync(String query) {
      return apiAsync(Utils.createMap(
              "method", "fql.query",
              "query", query));
   }

   private void prepareOauthRequestParams(Map<String,String> params) {
      if (params.get("access_token") == null) {
         params.put("access_token", getAccessToken());
      }
      // TODO parameter values, if not strings, must be json-encoded
//      foreach ($params as $key => $value) {
//        if (!is_string($value)) {
//          $params[$key] = json_encode($value);
//        }
//      }
   }

   /**
    * Make a OAuth Request
    *
    * @param url the path (required)
    * @param params the query/post data
    * @return the Json response object
    */
   protected JsonElement oauthRequest(String url, Map<String,String> params) throws FacebookApiException {
      prepareOauthRequestParams(params);
      return makeRequest(url, params);
   }

   protected Future<WS.HttpResponse> oauthAsyncRequest(String url, Map<String, String> params) {
      prepareOauthRequestParams(params);
      return makeAsyncRequest(url, params);
   }

   private WS.WSRequest createRequest(String url, Map<String,String> params) {
      // mimic facebook php library
      return WS.url(url).setHeader("User-Agent","facebook-php-2.0").setParameters(params);
   }

  /**
    * Makes an HTTP request. This method can be overriden by subclasses if
    * developers want to do fancier things or use something other than curl to
    * make the request.
    *
    * @param url the URL to make the request to
    * @param params the parameters to use for the POST body
    * @return the Json response element
    */
   protected JsonElement makeRequest(String url, Map<String,String> params) throws FacebookApiException {
      WS.WSRequest request = createRequest(url, params);
      return getResponseAsJson(request.post());
   }

   protected Future<WS.HttpResponse> makeAsyncRequest(String url, Map<String, String> params) {
      WS.WSRequest request = createRequest(url, params);
      return request.postAsync();
   }

   public JsonElement getResponseAsJson(WS.HttpResponse response) throws FacebookApiException {
      JsonElement json = response.getJson();
      // results are returned, errors are thrown
      // Handle errors here rather than above, since apiAsync results in this function being called for the response
      // TODO is it ok that we detect both _graph and _restserver -type errors here?
      // if not, we'll need to add a parameter to distinguish

      if (json.isJsonObject()) {
         JsonObject jsonObject = (JsonObject)json;
         if (jsonObject.get("error_code") != null) {
            FacebookApiException e= new FacebookApiException(jsonObject);
            if (e.getCode().equals("190")) { // Invalid OAuth 2.0 Access Token
               setSession(null);  // php-sdk doesn't do this, but it seems to be necessary in my (very limited) tests, and I don't see how it would hurt
            }
            throw e;
         }
         else if (jsonObject.get("error") != null) {
            FacebookApiException e = new FacebookApiException(jsonObject);
            if (e.getType().equals("OAuthException") || e.getType().equals("invalid_token")) {
               setSession(null);
            }
            throw e;
         }
      }

      // TODO are there other HttpResponse errors?  What do we do about CURLE_SSL_CACERT?
//      if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
//        self::errorLog('Invalid or no certificate authority found, using bundled information');
//        curl_setopt($ch, CURLOPT_CAINFO,
//                    dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
//        $result = curl_exec($ch);
//      }
//
//      if ($result === false) {
//        $e = new FacebookApiException(array(
//          'error_code' => curl_errno($ch),
//          'error'      => array(
//            'message' => curl_error($ch),
//            'type'    => 'CurlException',
//          ),
//        ));
//        curl_close($ch);
//        throw $e;
//      }
//      curl_close($ch);

      return json;
   }

  /**
    * The name of the Cookie that contains the session.
    *
    * @return the cookie name
    */
   protected String getSessionCookieName() {
      return "fbs_"+getAppId();
   }

  /**
    * Set a JS Cookie based on the _passed in_ session. It does not use the
    * currently stored session -- you need to explicitly pass it in.
    *
    * @param sess the session to use for setting the cookie
    */
   protected void setCookieFromSession(Session sess) {
      if (!useCookieSupport()) {
         return;
      }

      String cookieName = getSessionCookieName();
      String value = "deleted";
      int maxAge = (int)(new Date().getTime() / 1000);
      String domain = getBaseDomain();
      if (sess != null) {
         value = Utils.http_build_query(sess.asSortedMap());
         if (sess.getBaseDomain() != null) {
            domain = sess.getBaseDomain();
         }
         try {
            maxAge = Integer.parseInt(sess.getExpires());
         }
         catch (NumberFormatException e) {
            throw new UnexpectedException(e);
         }
      }

      if (domain.length() > 0) {
         domain = "."+domain;
      }

      Http.Cookie cookie = Http.Request.current().cookies.get(cookieName);
      if ("deleted".equals(value) && cookie == null) {
         return;
      }

      // TODO is there a way to tell if headers have been sent?
//      if (headers_sent()) {
//        self::errorLog('Could not set cookie. Headers already sent.');

      cookie = new Http.Cookie();
      cookie.name = cookieName;
      cookie.domain = domain;
      cookie.path = "/";
      cookie.value = value;
      cookie.maxAge = maxAge;
      Http.Response.current().cookies.put(cookieName, cookie);
   }

 /**
    * Validates a session_version=3 style session object.
    *
    * @param sess the session object
    * @return the session object if it validates, null otherwise
    */
   protected Session validateSessionObject(Session sess) {
     // make sure some essential fields exist
      if (sess != null &&
          sess.getUid() != null &&
          sess.getAccessToken() != null &&
          sess.getSignature() != null) {
         // validate the signature
         try {
            Session sessionWithoutSignature = sess.clone();
            sessionWithoutSignature.setSignature(null);
            String expectedSig = generateSignature(sessionWithoutSignature, getApiSecret());
            if (!expectedSig.equals(sess.getSignature())) {
               Logger.error("Got invalid session signature in cookie.");
               sess = null;
            }
         } catch (CloneNotSupportedException e) {
            throw new UnexpectedException(e);
         }
         // check expiry time
      }
      else {
         sess = null;
      }
      return sess;
   }

 /**
    * Returns something that looks like our JS session object from the
    * signed token's data
    *
    * TODO: Nuke this once the login flow uses OAuth2
    *
    * @param signedRequest the output of getSignedRequest
    * @return session
    */
   protected Session createSessionFromSignedRequest(SignedRequest signedRequest) {
      if (signedRequest.getOauthToken() == null) {
         return null;
      }

      Session sess = new Session(signedRequest.getUid(), signedRequest.getOauthToken(), signedRequest.getExpires());
      // put a real sig, so that validateSignature works
      sess.setSignature(generateSignature(sess, getApiSecret()));

      return sess;
   }

 /**
    * Parses a signed_request and validates the signature.
    *
    * @param signedRequestString A signed token
    * @return SignedRequest the payload inside it or null if the sig is wrong
    */
 protected SignedRequest parseSignedRequest(String signedRequestString) {
    String[] fields = signedRequestString.split("\\.", 2);
    String encodedSig = fields[0];
    String payload = fields[1];
    // decode the data
    try {
       byte[] sig = base64UrlDecode(encodedSig);
       String decodedPayload = new String(base64UrlDecode(fields[1]), "UTF-8");
       SignedRequest signedRequest = new SignedRequest(new JsonParser().parse(decodedPayload));
       if (!"HMAC-SHA256".equals(signedRequest.getAlgorithm().toUpperCase())) {
          play.Logger.error("Got invalid session signature in cookie.");
          return null;
       }

       // check sig
       byte[] expectedSig = Utils.hash_hmac_sha256(payload, getApiSecret());
       if (!Arrays.equals(sig, expectedSig)) {
          play.Logger.error("Bad Signed JSON signature!");
          return null;
       }
       return signedRequest;

    } catch (UnsupportedEncodingException e) {
       throw new UnexpectedException(e);
    }
 }

   private static final String[] READ_ONLY_CALLS_ARRAY = {
      "admin.getallocation",
     "admin.getappproperties",
     "admin.getbannedusers",
     "admin.getlivestreamvialink",
     "admin.getmetrics",
     "admin.getrestrictioninfo",
     "application.getpublicinfo",
     "auth.getapppublickey",
     "auth.getsession",
     "auth.getsignedpublicsessiondata",
     "comments.get",
     "connect.getunconnectedfriendscount",
     "dashboard.getactivity",
     "dashboard.getcount",
     "dashboard.getglobalnews",
     "dashboard.getnews",
     "dashboard.multigetcount",
     "dashboard.multigetnews",
     "data.getcookies",
     "events.get",
     "events.getmembers",
     "fbml.getcustomtags",
     "feed.getappfriendstories",
     "feed.getregisteredtemplatebundlebyid",
     "feed.getregisteredtemplatebundles",
     "fql.multiquery",
     "fql.query",
     "friends.arefriends",
     "friends.get",
     "friends.getappusers",
     "friends.getlists",
     "friends.getmutualfriends",
     "gifts.get",
     "groups.get",
     "groups.getmembers",
     "intl.gettranslations",
     "links.get",
     "notes.get",
     "notifications.get",
     "pages.getinfo",
     "pages.isadmin",
     "pages.isappadded",
     "pages.isfan",
     "permissions.checkavailableapiaccess",
     "permissions.checkgrantedapiaccess",
     "photos.get",
     "photos.getalbums",
     "photos.gettags",
     "profile.getinfo",
     "profile.getinfooptions",
     "stream.get",
     "stream.getcomments",
     "stream.getfilters",
     "users.getinfo",
     "users.getloggedinuser",
     "users.getstandardinfo",
     "users.hasapppermission",
     "users.isappuser",
     "users.isverified",
     "video.getuploadlimits"
   };
   private static final Set<String> READ_ONLY_CALLS = new HashSet<String>(Arrays.asList(READ_ONLY_CALLS_ARRAY));

 /**
    * Build the URL for api given parameters.
    *
    * @param method the method name.
    * @return the URL for the given parameters
    */
   protected String getApiUrl(String method) {
     String name = (READ_ONLY_CALLS.contains(method.toLowerCase()) ? "api_read" : "api");
     return getUrl(name, "restserver.php");
   }

 /**
    * Build the URL for given domain alias, path and parameters.
    *
    * @param name the name of the domain
    * @param path optional path (without a leading slash)
    * @param params optional query parameters
    * @return  the URL for the given parameters
    */
   protected String getUrl(String name, String path, Map<String,String> params) {
      StringBuilder buf = new StringBuilder();
      buf.append(DOMAIN_MAP.get(name));
      if (path != null) {
         if (path.charAt(0) == '/') {
            path = path.substring(1);
         }
         buf.append(path);
      }
      if (params.size() > 0) {
         buf.append('?');
         buf.append(Utils.http_build_query(params));
      }
      return buf.toString();
   }

   protected String getUrl(String name, String path) {
      return getUrl(name, path, Collections.<String,String>emptyMap());
   }

   protected static Set<String> DROP_QUERY_PARAMS = new HashSet<String>(Arrays.asList("session", "signed_request"));

   /**
    * Returns the Current URL, stripping it of known FB parameters that should
    * not persist.
    *
    * @return String the current URL
    */
   protected String getCurrentUrl() {
      Http.Request request = Http.Request.current();
      String protocol = (request.secure ? "https://" : "http://");
      String host = request.host;
      // remove the port
      int pos = host.indexOf(':');
      if (pos > 0) host = host.substring(0, pos);
      String port = Integer.toString(request.port);
      String path = request.path;
      String query = request.querystring;

      // drop known fb params
      if (query.length() > 0) {
         Map<String,String[]> params = Utils.parse_str(query);
         Map<String,String> buf = new HashMap<String,String>();
         for (String key : params.keySet()) {
            if (!DROP_QUERY_PARAMS.contains(key)) {
               buf.put(key, params.get(key)[0]);
            }
         }
         query = Utils.http_build_query(buf);
      }

      // use port if non default
      port = (port.length() > 0 &&
              ((!request.secure && !port.equals("80")) ||
               (request.secure && !port.equals("443"))) ? port : "");

      //rebuild
      StringBuilder buf = new StringBuilder();
      buf.append(protocol);
      buf.append(host);
      if (port.length() > 0) {
         buf.append(":");
         buf.append(port);
      }
      buf.append(path);
      if (query.length() > 0) {
         buf.append("?");
         buf.append(query);
      }
      return buf.toString();
   }

   /**
    * Generate a signature for the given params and secret.
    *
    * @param sess the session to sign
    * @param secret the secret to sign with
    * @return the generated signature
    */
   protected static String generateSignature(Session sess, String secret) {
      // work with sorted data
      SortedMap<String,String> params = sess.asSortedMap();

      // generate the base string
      StringBuilder buf = new StringBuilder();
      for(String key : params.keySet()) {
         buf.append(key);
         buf.append('=');
         buf.append(params.get(key));
      }
      buf.append(secret);
      return Utils.md5(buf.toString());
   }

   protected static byte[] base64UrlDecode(String s) {
      return Base64.decodeBase64(s.replace('-', '+').replace('_', '/'));
   }

   public static class FacebookApiException extends Exception {
      private String type = null;
      private String code = null;

      public FacebookApiException(String error) {
         super(error);
      }

      public FacebookApiException(JsonObject json) {
         super(createMessage(extractCode(json), extractMessage(json), extractType(json)));
         code = extractCode(json);
         type = extractType(json);
      }

      public FacebookApiException(String code, String message, String type) {
         super(createMessage(code, message, type));
         this.code = code;
         this.type = type;
      }

      public String getType() {
         return type;
      }

      public String getCode() {
         return code;
      }

      private static String createMessage(String code, String message, String type) {
         return type+": " + (code != null ? code+": " : "") + message;
      }

      private static String extractCode(JsonObject json) {
         JsonElement e = json.get("error_code");
         if (e != null && e.isJsonPrimitive()) {
            return ((JsonPrimitive)e).getAsString();
         }
         return null;
      }

      private static String extractMessage(JsonObject json) {
         JsonElement e = json.get("error_description");
         if (e != null && e.isJsonPrimitive()) {
            // OAuth 2.0 Draft 10 style
            return ((JsonPrimitive)e).getAsString();
         }
         e = json.get("error");
         if (e != null && e.isJsonObject()) {
            // OAuth 2.0 Draft 00 style
            e = ((JsonObject)e).get("message");
            if (e != null && e.isJsonPrimitive()) {
               return ((JsonPrimitive)e).getAsString();
            }
         }
         e = json.get("error_msg");
         if (e != null && e.isJsonPrimitive()) {
            // Rest server style
            return ((JsonPrimitive)e).getAsString();
         }
         return "Unknown Error. Check getResult()";
      }

      private static String extractType(JsonObject json) {
         JsonElement e = json.get("error");
         if (e != null) {
            if (e.isJsonPrimitive()) {
               // OAuth 2.0 Draft 10 style
               return ((JsonPrimitive)e).getAsString();
            }
            else if (e.isJsonObject()) {
               // OAuth 2.0 Draft 00 style
               e = ((JsonObject)e).get("type");
               if (e != null && e.isJsonPrimitive()) {
                  return ((JsonPrimitive)e).getAsString();
               }
            }
         }
         return "Exception";
      }
   }
}
